using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace Microsoft.Crm.Passport.IdCrlWrapper
{
    /// <summary>
    /// Sample program that shows how to obtain Passport tickets for an arbitrary Passport service.
    /// </summary>
    /// <remarks>
    /// Ensure the following prerequisites are met:
    /// 	- Make sure that idcrl40.dll is present in your application's execution path.
    ///		- Make sure you have imported registry keys specified by file environments.reg.
    ///		- Make sure that the obtained Passport tickets are cached for performance reasons.
    /// Change sample parameters to use the following for Microsoft Dynamics Live CRM:
    ///		- Specify a registered user name and password instead of the sample parameters used below.
    ///		- Specify "CRMLive" instead of "monitoring.passport-int.com" as the Passport partner name.
    ///		- Specify "NFS_2HR_0_COMPACT" instead of "?id=39000" as the Passport partner policy name.
    ///		- Specify "PPE" instead of "INT" as the Passport environment name.
    /// Note the following limitations:
    ///		- This code is sample code, it is provided "as is" with no warranties, and confers no rights.
    ///		- This code should be compiled against Microsoft .NET 2.0 or greater.
    ///		- This code must be compiled in 32 bit mode because idcrl40.dll is a 32 bit binary.
    ///		- This code cannot run under a network service context because idcrl40.dll will load cached data in the %application data% folder.
    ///		- This code is not thread safe.
    /// Note the following intended usage:
    ///		- After Passport tickets have been obtained using this sample code, they should be converted into CRM tickets.
    ///		- TODO: add more information on how to do this...
    /// </remarks>
    public sealed class Program
    {
        /// <summary>
        /// Main method.
        /// </summary>
        public static void Main()
        {
            string passportTicket = null;
            using (LogonManager logonManager = new LogonManager())
            {
                passportTicket = logonManager.Logon("IDCRLUser@hotmail-int.com", "password", "monitoring.passport-int.com", "?id=39000", "INT");
                Console.WriteLine(passportTicket);
            }
        }
    }

    /// <summary>
    /// Sample class capable of obtaining Passport tickets for an arbitrary Passport service.
    /// </summary>
    public sealed class LogonManager : IDisposable
    {
        /// <summary>
        /// Disposed state.
        /// </summary>
        private bool disposed = false;

        /// <summary>
        /// Identity handle.
        /// </summary>
        private IntPtr identityPtr = IntPtr.Zero;

        /// <summary>
        /// Service id (an arbitrary id).
        /// </summary>
        private Guid serviceGuid = Guid.NewGuid();

        /// <summary>
        /// Initialized status.
        /// </summary>
        private bool initialized = false;

        /// <summary>
        /// Password credential type.
        /// </summary>
        private const string credTypePassword = "ps:password"; /* PPCRL_CREDTYPE_PASSWORD */

        /// <summary>
        /// Constructor.
        /// </summary>
        public LogonManager()
        {
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~LogonManager()
        {
            // Dispose
            Debug.Assert(false, @"The finalizer for the LogonManager instance should not be called directly, use a ""using"" statement instead.");
            this.Dispose(false);
        }

        /// <summary>
        /// Disposes the object.
        /// </summary>
        public void Dispose()
        {
            // Dispose
            this.Dispose(true);
        }

        /// <summary>
        /// Diposes the object.
        /// </summary>
        /// <param name="disposing">True if disposing, otherwise, false.</param>
        private void Dispose(bool disposing)
        {
            // Throw if re-disposing.
            if (disposed)
            {
                throw new ApplicationException("The LogonManager instance should not already have been disposed.");
            }

            // Close the identity.
            this.CloseIdentity();

            // Uninitialize the system.
            this.Uninitialize();

            // Set the instance to disposed.
            disposed = true;
            if (disposing)
            {
                GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Logs on to the specified service and returns a Passport ticket.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <param name="password">Password.</param>
        /// <param name="partner">Partner.</param>
        /// <param name="policy">Policy.</param>
        /// <param name="environment">Environment.</param>
        /// <returns>Passport ticket.</returns>
        public string Logon(string userName, string password, string partner, string policy, string environment)
        {
            // Data validation.
            Debug.Assert(!String.IsNullOrEmpty(userName));
            Debug.Assert(!String.IsNullOrEmpty(partner));

            // Close the identity.
            this.CloseIdentity();

            // Initialize the system.
            this.Initialize(environment);

            // Open a new identity.
            this.OpenIdentity(userName, password);

            // Log on to Passport.
            this.LogonPassport(policy);

            // Get a Passport ticket for the specified service.
            return this.LogonService(partner, policy);
        }

        /// <summary>
        /// Re-initializes the system.
        /// </summary>
        /// <param name="environment">Environment name.</param>
        private void Initialize(string environment)
        {
            // Declarations
            int hresult = 0;

            // Normalize the environment.
            if (null == environment)
            {
                environment = String.Empty;
            }

            // Un-initialize the system.
            this.Uninitialize();

            // Prepare system initialization options.
            IDCRL_OPTION[] options = null;
            uint optionsCount = 0;
            GCHandle environmentBufferHandle = new GCHandle();
            try
            {
                // Set the environment option.
                if (!String.IsNullOrEmpty(environment))
                {
                    options = new IDCRL_OPTION[1];
                    optionsCount = 1;
                    byte[] environmentBytes = Encoding.Unicode.GetBytes(environment);
                    byte[] environmentBuffer = new byte[environmentBytes.Length];
                    environmentBufferHandle = GCHandle.Alloc(environmentBuffer, GCHandleType.Pinned);
                    IntPtr environmentBufferPtr = environmentBufferHandle.AddrOfPinnedObject();
                    Marshal.Copy(environmentBytes, 0, environmentBufferPtr, environmentBytes.Length);
                    options[0].dwId = 0x00000040 /* IDCRL_OPTION_ENVIRONMENT */;
                    options[0].pValue = environmentBufferPtr;
                    options[0].cbValue = (uint)environmentBytes.Length;
                }

                // Initialize the system.
                hresult = NativeMethods.InitializeEx(ref this.serviceGuid, 1 /* PPCRL_API_VERSION_CURRENT */, 0 /* DEFAULT_UPDATE_POLICY */, options, optionsCount);
                if (hresult < 0)
                {
                    throw new ApplicationException("InitializeEx() failed.");
                }
                this.initialized = true;
            }
            finally
            {
                // Free resources.
                if (environmentBufferHandle.IsAllocated)
                {
                    environmentBufferHandle.Free();
                }
            }
        }

        /// <summary>
        /// Uninitializes the system if needed.
        /// </summary>
        private void Uninitialize()
        {
            // Declarations
            int hresult = 0;

            // Check if uninitialization is needed.
            if (!this.initialized)
            {
                return;
            }

            // Uninitialize the system.
            hresult = NativeMethods.Uninitialize();
            if (hresult < 0)
            {
                throw new ApplicationException("Uninitialize() failed.");
            }
            this.initialized = false;
        }

        /// <summary>
        /// Opens an identity.
        /// </summary>
        /// <param name="userName">User.</param>
        /// <param name="password">Password.</param>
        private void OpenIdentity(string userName, string password)
        {
            // Data validation.
            Debug.Assert(!String.IsNullOrEmpty(userName));
            Debug.Assert(IntPtr.Zero == this.identityPtr);

            // Declarations
            int hresult = 0;

            // Open an identity.
            try
            {
                // Create an identity handle.
                hresult = NativeMethods.CreateIdentityHandle(userName, 0x000000ff /* IDENTITY_SHARE_ALL */, out this.identityPtr);
                if (hresult < 0)
                {
                    throw new ApplicationException("CreateIdentityHandle() failed.");
                }
                Debug.Assert(IntPtr.Zero != this.identityPtr);

                // Set identity credentials if specified.
                if (!String.IsNullOrEmpty(password))
                {
                    hresult = NativeMethods.SetCredential(this.identityPtr, credTypePassword /* PPCRL_CREDTYPE_PASSWORD */, password);
                    if (hresult < 0)
                    {
                        throw new ApplicationException("SetCredential() failed.");
                    }
                }
            }
            catch
            {
                // Close the identity (ignore all errors at this point).
                try { this.CloseIdentity(); }
                catch { }
                throw;
            }
        }

        /// <summary>
        /// Closes the current identity as needed.
        /// </summary>
        private void CloseIdentity()
        {
            // Declarations
            int hresult = 0;

            // Close the identify handle.
            if (IntPtr.Zero != this.identityPtr)
            {
                hresult = NativeMethods.CloseIdentityHandle(this.identityPtr);
                if (hresult < 0)
                {
                    throw new ApplicationException("CloseIdentityHandle() failed.");
                }
                this.identityPtr = IntPtr.Zero;
            }
        }

        /// <summary>
        /// Logs on to Passport.
        /// </summary>
        /// <param name="policy">Policy.</param>
        private void LogonPassport(string policy)
        {
            // Data validation.
            Debug.Assert(IntPtr.Zero != this.identityPtr);

            // Declarations
            int hresult = 0;

            // Log on the identity.
            hresult = NativeMethods.LogonIdentity(this.identityPtr, policy, 0 /* LOGONIDENTITY_DEFAULT */);
            if (hresult < 0)
            {
                throw new ApplicationException("LogonIdentity() failed.");
            }

            // Get the authentication state.
            int authState = 0;
            int authRequired = 0;
            int requestStatus = 0;
            IntPtr webFlowUrl = IntPtr.Zero;
            hresult = NativeMethods.GetAuthState(this.identityPtr, out authState, out authRequired, out requestStatus, out webFlowUrl);
            if (hresult < 0)
            {
                throw new ApplicationException("GetAuthState() failed.");
            }

            // Free unused authenticate state data.
            this.FreeResource(ref webFlowUrl);

            // Verify the authentication state.
            if (296963 /* PPCRL_AUTHSTATE_S_AUTHENTICATED_PASSWORD */ != authState)
            {
                throw new ApplicationException("GetAuthState() failed.");
            }
        }

        /// <summary>
        /// Logs on to a partner service and returns a ticket.
        /// </summary>
        /// <param name="partner">Partner.</param>
        /// <param name="policy">Policy.</param>
        /// <returns>Passport ticket.</returns>
        private string LogonService(string partner, string policy)
        {
            // Data validation.
            Debug.Assert(IntPtr.Zero != this.identityPtr);
            Debug.Assert(!String.IsNullOrEmpty(partner));

            // Declarations
            int hresult = 0;

            // Log on to the service.
            IntPtr tokenPtr = IntPtr.Zero;
            IntPtr sessionKeyPtr = IntPtr.Zero;
            uint sessionKeyLength = 0;
            uint resultFlags = 0;
            hresult = NativeMethods.AuthIdentityToService(this.identityPtr, partner, policy, 0 /* SERVICE_TOKEN_REQUEST_TYPE_NONE */, out tokenPtr, out resultFlags, out sessionKeyPtr, out sessionKeyLength);
            if (hresult < 0)
            {
                throw new ApplicationException("AuthIdentityToService() failed.");
            }

            // Return the obtained ticket.
            Debug.Assert(IntPtr.Zero != tokenPtr);
            try
            {
                return Marshal.PtrToStringUni(tokenPtr);
            }
            finally
            {
                // Always try to free both resources even at the cost of eating exceptions on free.
                try { this.FreeResource(ref tokenPtr); }
                catch { }
                try { this.FreeResource(ref sessionKeyPtr); }
                catch { }
            }
        }

        /// <summary>
        /// Frees resources allocated by IDCRL as needed.
        /// </summary>
        /// <param name="resource">Resource to free.</param>
        private void FreeResource(ref IntPtr resource)
        {
            // Declarations
            int hresult = 0;

            // Free resources.
            if (IntPtr.Zero != resource)
            {
                hresult = NativeMethods.PassportFreeMemory(resource);
                if (hresult < 0)
                {
                    throw new ApplicationException("PassportFreeMemory() failed.");
                }
                resource = IntPtr.Zero;
            }
        }
    }

    /// <summary>
    /// Option structure.
    /// </summary>
    /// <remarks>Members are named as per the IDCRL C++ header file.</remarks>
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    struct IDCRL_OPTION
    {
        /// <summary>
        /// Option id.
        /// </summary>
        public int dwId;

        /// <summary>
        /// Option value.
        /// </summary>
        public IntPtr pValue;

        /// <summary>
        /// Option size.
        /// </summary>
        public uint cbValue;
    };

    /// <summary>
    /// Native methods wrapping the IDCRL unmanaged DLL.
    /// </summary>
    static class NativeMethods
    {
        [DllImport("msidcrl40.dll", EntryPoint = "InitializeEx", CharSet = CharSet.Unicode)]
        internal static extern int InitializeEx([In] ref System.Guid guid, [In] int lPPCRLVersion, [In] uint dwFlags, [In, MarshalAs(UnmanagedType.LPArray)] IDCRL_OPTION[] pOptions, [In] uint dwOptions);

        [DllImport("msidcrl40.dll", EntryPoint = "CreateIdentityHandle", CharSet = CharSet.Unicode)]
        internal static extern int CreateIdentityHandle([In, Optional, MarshalAs(UnmanagedType.LPWStr)] string wszMemberName, [In] uint dwFlags, out IntPtr pihIdentity);

        [DllImport("msidcrl40.dll", EntryPoint = "SetCredential", CharSet = CharSet.Unicode)]
        internal static extern int SetCredential([In] IntPtr hIdentity, [In, MarshalAs(UnmanagedType.LPWStr)] string wszCredType, [In, MarshalAs(UnmanagedType.LPWStr)] string wszCredValue);

        [DllImport("msidcrl40.dll", EntryPoint = "LogonIdentity", CharSet = CharSet.Unicode)]
        internal static extern int LogonIdentity([In] IntPtr hIdentity, [In, MarshalAs(UnmanagedType.LPWStr)] string wszPolicy, uint dwAuthFlags);

        [DllImport("msidcrl40.dll", EntryPoint = "GetAuthState", CharSet = CharSet.Unicode)]
        internal static extern int GetAuthState([In] IntPtr hIdentity, out int phrAuthState, out int phrAuthRequired, out int phrRequestStatus, out IntPtr szWebFlowUrl);

        [DllImport("msidcrl40.dll", EntryPoint = "AuthIdentityToService", CharSet = CharSet.Unicode)]
        internal static extern int AuthIdentityToService([In] IntPtr hIdentity, [In, MarshalAs(UnmanagedType.LPWStr)] string szServiceTarget, [In, Optional, MarshalAs(UnmanagedType.LPWStr)] string szServicePolicy, [In] uint dwTokenRequestFlags, out IntPtr szToken, out uint pdwResultFlags, out IntPtr ppbSessionKey, out uint pcbSessionKeyLength);

        [DllImport("msidcrl40.dll", EntryPoint = "CloseIdentityHandle", CharSet = CharSet.Unicode)]
        internal static extern int CloseIdentityHandle([In] IntPtr hIdentity);

        [DllImport("msidcrl40.dll", EntryPoint = "Uninitialize", CharSet = CharSet.Unicode)]
        internal static extern int Uninitialize();

        [DllImport("msidcrl40.dll", EntryPoint = "PassportFreeMemory", CharSet = CharSet.Unicode)]
        internal static extern int PassportFreeMemory([In, Out] IntPtr pMemoryToFree);
    }
}
